> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.rhombus.community/llms.txt
> Use this file 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](/implementations/iot-sensors) guide. This page's "people counting" is *camera* analytics.
</Note>

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

## 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 under Settings > API.
  * 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.
</Note>

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.

<CodeGroup>
  ```python Python theme={null}
  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 theme={null}
  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 theme={null}
  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"}'
  ```
</CodeGroup>

<ParamField body="name" type="string" required>
  Display name for the person.
</ParamField>

The response returns a `person` object:

<ResponseField name="uuid" type="string">Unique identifier for the person.</ResponseField>
<ResponseField name="name" type="string">Display name.</ResponseField>
<ResponseField name="email" type="string">Optional email associated with the person.</ResponseField>
<ResponseField name="orgUuid" type="string">Organization the person belongs to.</ResponseField>
<ResponseField name="createdOn" type="string">When the person record was created.</ResponseField>
<ResponseField name="updatedOn" type="string">When the person record was last updated.</ResponseField>

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

<CodeGroup>
  ```python Python theme={null}
  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 theme={null}
  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 theme={null}
  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"
  ```
</CodeGroup>

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

The response returns:

<ResponseField name="transactionId" type="string">The transaction ID for this upload (echoed back, or generated if you omitted it). Use it to poll processing status.</ResponseField>
<ResponseField name="fileUploadResults" type="array">Per-file upload results, each with `fileName`, `success` (boolean), and `message`.</ResponseField>

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

<CodeGroup>
  ```python Python theme={null}
  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 theme={null}
  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 theme={null}
  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"}'
  ```
</CodeGroup>

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

### Label a person

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

<CodeGroup>
  ```python Python theme={null}
  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 theme={null}
  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 theme={null}
  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"}'
  ```
</CodeGroup>

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

<CodeGroup>
  ```python Python theme={null}
  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 theme={null}
  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 theme={null}
  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 '{}'
  ```
</CodeGroup>

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`                | `{ "personUuid": "..." }`                                       | Fetch one person                 |
| `/api/faceRecognition/person/updatePerson`             | `{ "personSelectiveUpdate": { "uuid": "...", "name": "..." } }` | Update name/email (selective)    |
| `/api/faceRecognition/person/deletePerson`             | `{ "personUuid": "..." }`                                       | Delete a person                  |
| `/api/faceRecognition/matchmaker/getFaceMatchmaker`    | `{ "faceId": "..." }`                                           | Fetch one enrolled face          |
| `/api/faceRecognition/matchmaker/deleteFaceMatchmaker` | `{ "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).
</Note>

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

<CodeGroup>
  ```python Python theme={null}
  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 theme={null}
  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 theme={null}
  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 }
    }'
  ```
</CodeGroup>

### Search filter

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

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

### Page request

<ParamField body="maxPageSize" type="integer">Maximum number of events to return in one page.</ParamField>
<ParamField body="lastEvaluatedKey" type="string">Pagination cursor. Pass the `lastEvaluatedKey` from the previous response to fetch the next page. Absent when there are no more results.</ParamField>

### Interpreting a face event

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

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

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

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

<CodeGroup>
  ```python Python theme={null}
  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 theme={null}
  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 theme={null}
  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}'
  ```
</CodeGroup>

<ParamField body="faceMatchConfidenceThreshold" type="number">Minimum match confidence (0–1) required to consider a detected face a match.</ParamField>

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

<CodeGroup>
  ```python Python theme={null}
  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 theme={null}
  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 theme={null}
  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"
    }'
  ```
</CodeGroup>

<ParamField body="eventUuid" type="string" required>The face event to update.</ParamField>
<ParamField body="personUuid" type="string">UUID of the correct person to associate with the event.</ParamField>
<ParamField body="personName" type="string">Name to associate with the event. Ignored if `personUuid` matches one of the event's top person matches.</ParamField>

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"]`.

<CodeGroup>
  ```python Python theme={null}
  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 theme={null}
  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 theme={null}
  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"
    }'
  ```
</CodeGroup>

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

Each entry in `timeSeriesDataPoints` includes:

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

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

### Most recent people counts

`getMostRecentPeopleCountEvents` returns the latest raw people-count events for a camera — useful for a live headcount tile.

<CodeGroup>
  ```python Python theme={null}
  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/report/getMostRecentPeopleCountEvents",
      headers=headers,
      json={"deviceUuid": "YOUR_CAMERA_UUID", "numMostRecent": 10}
  )

  for event in response.json().get("events", []):
      print(f"{event.get('eventTimestamp')}: {event.get('peopleCount')} people")
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://api2.rhombussystems.com/api/report/getMostRecentPeopleCountEvents', {
    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', numMostRecent: 10 })
  });

  const data = await response.json();
  for (const event of data.events || []) {
    console.log(`${event.eventTimestamp}: ${event.peopleCount} people`);
  }
  ```

  ```bash cURL theme={null}
  curl -X POST https://api2.rhombussystems.com/api/report/getMostRecentPeopleCountEvents \
    -H "Content-Type: application/json" \
    -H "x-auth-scheme: api-token" \
    -H "x-auth-apikey: YOUR_API_KEY" \
    -d '{"deviceUuid": "YOUR_CAMERA_UUID", "numMostRecent": 10}'
  ```
</CodeGroup>

<ParamField body="deviceUuid" type="string" required>Camera to read people counts from.</ParamField>
<ParamField body="numMostRecent" type="integer" required>Number of most recent count events to return.</ParamField>

Each event includes `eventTimestamp` (epoch ms), `peopleCount`, `deviceUuid`, `locationUuid`, and `uuid`.

### Occupancy counts over time

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

<CodeGroup>
  ```python Python theme={null}
  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 theme={null}
  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 theme={null}
  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"
    }'
  ```
</CodeGroup>

<ParamField body="deviceUuid" type="string" required>Camera to read occupancy counts from.</ParamField>
<ParamField body="startTimeMs" type="integer" required>Start of the range in epoch milliseconds.</ParamField>
<ParamField body="endTimeMs" type="integer" required>End of the range in epoch milliseconds.</ParamField>
<ParamField body="interval" type="string" required>Bucket size (`MINUTELY`, `QUARTERHOURLY`, `HOURLY`, `DAILY`, `WEEKLY`, `MONTHLY`).</ParamField>

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](/implementations/iot-sensors) guide — that is a separate hardware product with its own API.
</Note>

## 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.
* **Live occupancy tiles** — surface `getMostRecentPeopleCountEvents` for a near-real-time headcount display.

## Troubleshooting

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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`.
  </Accordion>

  <Accordion title="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`.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Reports & Analytics" icon="chart-line" href="/implementations/reports-analytics">
    Full count-report model, threshold-crossing counts, and CSV export
  </Card>

  <Card title="LPR & Vehicle" icon="car" href="/implementations/lpr-vehicle">
    Detect and search license plates and vehicles across your cameras
  </Card>

  <Card title="IoT Sensors" icon="temperature-half" href="/implementations/iot-sensors">
    Physical occupancy, climate, and environmental sensors
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/overview">
    Browse and test every Rhombus API endpoint
  </Card>
</CardGroup>
